package org.intellij.plugins.junit4;

import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.SourceFolder;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiManager;
import com.intellij.util.IncorrectOperationException;
import org.apache.log4j.Logger;

import javax.swing.*;
import java.util.*;

/**
 * Create a new class, writing new java file to disk.
 * <p/>
 * Run this command from within Application.runWriteAction().
 * <p/>
 * Much of the code for this class was lifted from the TestDox IntelliJApi class.
 *
 * @author Justin Tomich
 */
public class CreateTestClassCommand implements Runnable
{
    private static final String SELECT_DIR_MSG = "Which directory should the test be in?";
    private static final String SELECT_DIR_TITLE = "Choose directory";
    private static final String CREATE_MSG = "Ok to create test class?";
    private static final String CREATE_TITLE = "Create Test Class";
    private static final String CREATE_ERROR_FRAGMENT = "Could not create test ";
    private static final String TEST_DIR_ERROR = "You must configure a test directory";
    private static final int CANCEL = -1;

    private final Logger log = Logger.getLogger("CONSOLE-WARN");

    private final String className;
    private final String packageName;
    private final PsiManager psiManager;
    private final Module module;

    private PsiClass createdClass;


    public CreateTestClassCommand(String className, String packageName,
                                  PsiManager psiManager, Module module)
    {
        this.className = className;
        this.packageName = packageName;
        this.psiManager = psiManager;
        this.module = module;
    }


    /**
     * Get created class, or return null if no class has been created.
     *
     * @return created class, or null if the user bailed out.
     */
    public PsiClass getCreatedClass()
    {
        return createdClass;
    }


    /**
     * Run this command from within Application.runWriteAction().
     * <p/>
     * Note: the user is offered a dialog box to continue or stop.
     */
    public void run()
    {
        try
        {
            boolean ok = askUserOkToCreateClass();
            if (!ok) return;

            VirtualFile testsRoot = getTestRootDir();
            if (testsRoot == null) return; // user bailed
            PsiDirectory psiTestsRoot = psiManager.findDirectory(testsRoot);
            PsiDirectory testDir = createOrMoveToDir(psiTestsRoot, packageName);
            createdClass = testDir.createClass(className);
        }
        catch (IncorrectOperationException e)
        {
            String m = CREATE_ERROR_FRAGMENT + className + ": " + e.getMessage();
            Dialog.error(module, m);
        }
    }


    private boolean askUserOkToCreateClass()
    {
        Project p = module.getProject();
        Icon icon = Messages.getQuestionIcon();
        int ok = Messages.showYesNoDialog(p, CREATE_MSG, CREATE_TITLE, icon);
        return ok == DialogWrapper.OK_EXIT_CODE;
    }


    /**
     * Create the directory the java file will live in, or changePackage to it if it already
     * exiists.
     *
     * @param testRootDir   root test directory
     * @param targetPackage root test directory
     * @return directory test will be created in, as a PsiDirectory
     * @throws IncorrectOperationException hucks if it's busted
     */
    private PsiDirectory createOrMoveToDir(PsiDirectory testRootDir,
                                           String targetPackage)
            throws IncorrectOperationException
    {
        PsiDirectory current = testRootDir;
        StringTokenizer tokenizer = new StringTokenizer(targetPackage, ".");
        while (tokenizer.hasMoreTokens())
        {
            String packageElem = tokenizer.nextToken();
            PsiDirectory dir = current.findSubdirectory(packageElem);
            if (dir == null) current = current.createSubdirectory(packageElem);
            if (dir != null) current = dir;
        }

        return current;
    }


    /**
     * Get the root test directory to place the new test class in.
     *
     * @return VirtualFile of root test directory to host new file.
     */
    private VirtualFile getTestRootDir()
    {
        List<VirtualFile> testFolders = new ArrayList<VirtualFile>();
        ModuleRootManager rootMgr = ModuleRootManager.getInstance(module);
        ContentEntry[] entries = rootMgr.getContentEntries();

        for (ContentEntry entry : entries) add(entry, testFolders);

        // wish I had closures. anyway, sort test folders to let user pick.
        Collections.sort(testFolders, new Comparator<VirtualFile>()
        {
            public int compare(VirtualFile f1, VirtualFile f2)
            {
                if (f1.getPath().length() > f2.getPath().length()) return 1;
                if (f1.getPath().length() < f2.getPath().length()) return -1;
                return 0;
            }
        });

        log.warn("testFolders=" + testFolders);

        if (testFolders.size() == 0)
        {
            Dialog.error(module, TEST_DIR_ERROR);
            return null;
        }
        if (testFolders.size() == 1) return testFolders.get(0);
        return askUserPickTestDir(module, testFolders);
    }


    private VirtualFile askUserPickTestDir(Module m, List<VirtualFile> dirs)
    {
        assert dirs.size() > 1;
        Project p = m.getProject();
        Icon icon = Messages.getQuestionIcon();
        String[] dirStrs = stringArray(dirs);
        String defaultSelection = dirStrs[0];
        log.warn("askUser: directories to choose from=" +
                Arrays.toString(dirStrs));
        log.warn("askUser: default=" + defaultSelection);

        // virtual files to strings
        int choice = Messages.showChooseDialog(p, SELECT_DIR_MSG, SELECT_DIR_TITLE,
                icon, dirStrs, defaultSelection);
        log.warn("choice=" + choice);
        if (choice == CANCEL) return null;
        return dirs.get(choice);
    }


    /**
     * Return the list of VirtualFile dirs as an array of  Strings.
     *
     * @param dirs list of VirtualFile directories.
     * @return array of directory paths as arrays.
     */
    private String[] stringArray(List<VirtualFile> dirs)
    {
        List<String> dirStrs = new ArrayList<String>();
        for (VirtualFile dir : dirs)
        {
            dirStrs.add(dir.getPath());
        }
        return dirStrs.toArray(new String[dirStrs.size()]);
    }


    private void add(ContentEntry entry, List<VirtualFile> testFolders)
    {
        for (SourceFolder folder : entry.getSourceFolders())
        {
            VirtualFile file = folder.getFile();
            if (file == null) continue;
            if (!file.isDirectory()) continue;
            if (!file.isWritable()) continue;
            if (folder.isTestSource()) testFolders.add(file);
        }
    }

}
